require 'fileutils'
require 'optparse'
require 'yaml'
require "ostruct"
require "recursive-open-struct"
require 'require_all'

require_all Dir.glob(File.join(File.dirname(__FILE__), '**', '*.rb'))

module Rubtools
  class Runner
    attr_accessor :args, :option_parser, :options, :tools, :config

    # Initialize the rubtool
    # - Find all recipes
    # - Parse options
    #
    def initialize(args)
      @args = args
      @options = Hash.new
      @tools = Hash.new
      @matches = Array.new

      rubtools_config_path = File.join(Dir.home, '.rubtools.yml')
      config_hash = YAML::load_file(rubtools_config_path) if File.exists? rubtools_config_path
      @config = RecursiveOpenStruct.new config_hash, recurse_over_arrays: true if config_hash

      if @config
        tool_libs = []
        if @config.recipes && @config.recipes.any?
          for recipes_folder in @config.recipes
            if File.exists?(recipes_folder) && File.directory?(recipes_folder)
              tool_libs << Dir.glob(File.join(recipes_folder, '**', '*.rb'))
            end
          end
        end

        # Require recipes from paths in the config file
        require_all tool_libs.flatten
      end

      @option_parser = OptionParser.new do |opts|
        opts.banner = "Usage: rt constant:method [options]"

        opts.separator ""
        opts.separator "Common options:"

        opts.on_tail("-L", "--list", "List available commands") do |l|
          @options[:list] = l
        end

        opts.on_tail("-C", "--cmplt", "Complete command") do |c|
          @options[:cmplt] = c
        end

        opts.on_tail("-h", "--help", "Show help") do
          Logger.verbose opts
          exit
        end

        opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
          @options[:verbose] = v
        end

        opts.on_tail("--version", "Show rubTools version") do
          Logger.verbose Rubtools::VERSION
          exit
        end
      end

      begin
        @option_parser.parse!(args)
      rescue OptionParser::InvalidOption
        Logger.verbose @option_parser
        exit
      end
    end

    # Run!
    # - Find the best constant method passed in parameter
    # - Execute the method
    #
    def run!
      # Searching in tools libraries the method to call
      for constant in Rubtools::Tools.constants
        @tools[constant.downcase] = Array.new

        const = Rubtools::Tools.const_get(constant)
        unless const.are_methods_hidden?
          for method in const.available_methods
            method_hash = Hash.new
            method_hash[method] = Array.new

            for param in Rubtools::Tools.const_get(constant).instance_method(method).parameters
              method_hash[method] << param.last
            end

            @tools[constant.downcase] << method_hash
          end
        end
      end

      # Execute options before running recipes
      execute_options

      begin
        raise ArgumentError, "missing arguments" unless @args.any?
      rescue ArgumentError => e
        Logger.verbose e.message
        Logger.verbose @option_parser
        exit
      end

      req_const_method = @args.shift.downcase
      values = req_const_method.split(/\W/)
      constant = values.flatten.first.capitalize
      method = values.flatten.last
        
      if @options[:verbose] == true
        execute constant, method
      else
        begin
          execute constant, method
        rescue Interrupt
          Logger.verbose "Interrupted"
        rescue Exception => e
          Logger.error "#{e.class} => #{e.message}"
        end
      end
    end


    private

    ## Execute rubtools recipe
    # - constant
    # - method
    def execute constant, method
      Logger.verbose "Calling #{constant.downcase}::#{method.downcase}..."
      object = Rubtools::Tools.const_get(constant).new @config, @options

      if @args.any?
        object.method(method).call @args
      else
        object.method(method).call
      end
    end

    ## Check and execute options
    # - List available commands
    # - Complete commands
    #
    def execute_options

      for option in @options
        ## List available commands
        #
        if option.first == :list && option.last
          Logger.verbose "Available commands: \n\n"
          for constant in @tools.keys
            for method_hash in @tools[constant]
              params = method_hash.first.last.any? ? "(#{method_hash.first.last.join(", ")})" : nil
              Logger.success "#{constant}:#{method_hash.keys.first}#{params}"
            end

            if @tools[constant].any? && @tools.keys.last != constant
              Logger.verbose "\n"
            end
          end
          exit 0

        ## Complete command
        #
        elsif option.first == :cmplt && option.last
          args = @args.first.split(":")

          if args.size == 1
            for constant in @tools.keys
              if constant.to_s.start_with? args.first
                print constant.to_s + ":"
                exit 0
              end
            end
          elsif args.size == 2
            for constant in @tools.keys
              for method_hash in @tools[constant]
                method_name = method_hash.keys.first
                if method_name.to_s.start_with? args.last
                  print constant.to_s + ":" + method_name.to_s
                  exit 0
                end
              end
            end
          end
        end
      end
    end
  end
end
